( function () {
class CSMHelper extends THREE.Group {
  constructor(csm) {
    super();
    this.csm = csm;
    this.displayFrustum = true;
    this.displayPlanes = true;
    this.displayShadowBounds = true;
    const indices = new Uint16Array([0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7]);
    const positions = new Float32Array(24);
    const frustumGeometry = new THREE.BufferGeometry();
    frustumGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
    frustumGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3, false));
    const frustumLines = new THREE.LineSegments(frustumGeometry, new THREE.LineBasicMaterial());
    this.add(frustumLines);
    this.frustumLines = frustumLines;
    this.cascadeLines = [];
    this.cascadePlanes = [];
    this.shadowLines = [];
  }

  updateVisibility() {
    const displayFrustum = this.displayFrustum;
    const displayPlanes = this.displayPlanes;
    const displayShadowBounds = this.displayShadowBounds;
    const frustumLines = this.frustumLines;
    const cascadeLines = this.cascadeLines;
    const cascadePlanes = this.cascadePlanes;
    const shadowLines = this.shadowLines;

    for (let i = 0, l = cascadeLines.length; i < l; i++) {
      const cascadeLine = cascadeLines[i];
      const cascadePlane = cascadePlanes[i];
      const shadowLineGroup = shadowLines[i];
      cascadeLine.visible = displayFrustum;
      cascadePlane.visible = displayFrustum && displayPlanes;
      shadowLineGroup.visible = displayShadowBounds;
    }

    frustumLines.visible = displayFrustum;
  }

  update() {
    const csm = this.csm;
    const camera = csm.camera;
    const cascades = csm.cascades;
    const mainFrustum = csm.mainFrustum;
    const frustums = csm.frustums;
    const lights = csm.lights;
    const frustumLines = this.frustumLines;
    const frustumLinePositions = frustumLines.geometry.getAttribute('position');
    const cascadeLines = this.cascadeLines;
    const cascadePlanes = this.cascadePlanes;
    const shadowLines = this.shadowLines;
    this.position.copy(camera.position);
    this.quaternion.copy(camera.quaternion);
    this.scale.copy(camera.scale);
    this.updateMatrixWorld(true);

    while (cascadeLines.length > cascades) {
      this.remove(cascadeLines.pop());
      this.remove(cascadePlanes.pop());
      this.remove(shadowLines.pop());
    }

    while (cascadeLines.length < cascades) {
      const cascadeLine = new THREE.Box3Helper(new THREE.Box3(), 0xffffff);
      const planeMat = new THREE.MeshBasicMaterial({
        transparent: true,
        opacity: 0.1,
        depthWrite: false,
        side: THREE.DoubleSide
      });
      const cascadePlane = new THREE.Mesh(new THREE.PlaneGeometry(), planeMat);
      const shadowLineGroup = new THREE.Group();
      const shadowLine = new THREE.Box3Helper(new THREE.Box3(), 0xffff00);
      shadowLineGroup.add(shadowLine);
      this.add(cascadeLine);
      this.add(cascadePlane);
      this.add(shadowLineGroup);
      cascadeLines.push(cascadeLine);
      cascadePlanes.push(cascadePlane);
      shadowLines.push(shadowLineGroup);
    }

    for (let i = 0; i < cascades; i++) {
      const frustum = frustums[i];
      const light = lights[i];
      const shadowCam = light.shadow.camera;
      const farVerts = frustum.vertices.far;
      const cascadeLine = cascadeLines[i];
      const cascadePlane = cascadePlanes[i];
      const shadowLineGroup = shadowLines[i];
      const shadowLine = shadowLineGroup.children[0];
      cascadeLine.box.min.copy(farVerts[2]);
      cascadeLine.box.max.copy(farVerts[0]);
      cascadeLine.box.max.z += 1e-4;
      cascadePlane.position.addVectors(farVerts[0], farVerts[2]);
      cascadePlane.position.multiplyScalar(0.5);
      cascadePlane.scale.subVectors(farVerts[0], farVerts[2]);
      cascadePlane.scale.z = 1e-4;
      this.remove(shadowLineGroup);
      shadowLineGroup.position.copy(shadowCam.position);
      shadowLineGroup.quaternion.copy(shadowCam.quaternion);
      shadowLineGroup.scale.copy(shadowCam.scale);
      shadowLineGroup.updateMatrixWorld(true);
      this.attach(shadowLineGroup);
      shadowLine.box.min.set(shadowCam.bottom, shadowCam.left, -shadowCam.far);
      shadowLine.box.max.set(shadowCam.top, shadowCam.right, -shadowCam.near);
    }

    const nearVerts = mainFrustum.vertices.near;
    const farVerts = mainFrustum.vertices.far;
    frustumLinePositions.setXYZ(0, farVerts[0].x, farVerts[0].y, farVerts[0].z);
    frustumLinePositions.setXYZ(1, farVerts[3].x, farVerts[3].y, farVerts[3].z);
    frustumLinePositions.setXYZ(2, farVerts[2].x, farVerts[2].y, farVerts[2].z);
    frustumLinePositions.setXYZ(3, farVerts[1].x, farVerts[1].y, farVerts[1].z);
    frustumLinePositions.setXYZ(4, nearVerts[0].x, nearVerts[0].y, nearVerts[0].z);
    frustumLinePositions.setXYZ(5, nearVerts[3].x, nearVerts[3].y, nearVerts[3].z);
    frustumLinePositions.setXYZ(6, nearVerts[2].x, nearVerts[2].y, nearVerts[2].z);
    frustumLinePositions.setXYZ(7, nearVerts[1].x, nearVerts[1].y, nearVerts[1].z);
    frustumLinePositions.needsUpdate = true;
  }

}

THREE.CSMHelper = CSMHelper;
} )();
